import usePortal from "../use/usePortal";
import useAlignPostion from "../use/useAlignPostion";
import useClickOutside from "../use/useClickOutside";
import usezIndex from "../use/usezIndex";
import useTransition from "../use/useTransition";
import { PropType, Teleport, computed, defineComponent, h, onBeforeUnmount, onMounted, onUnmounted, provide, ref, watch, watchEffect } from "vue";
import DropdownItem, { DropdownItemProps } from "./DropdownItem";
import { useDebounce } from "../use/useDebounce";
import { isColor, uuidv4 } from "../utils/assist";
import { useMoveObserver } from "../use/useMoveObserver";
import DropdownMenu from "./DropdownMenu";

export { default as DropdownMenu } from './DropdownMenu';
export { default as DropdownItem } from './DropdownItem';

export interface DropdownPosition {
    x: number
    y: number
}

export interface DropdownNode extends DropdownItemProps {
    title: string
    children?: DropdownNode[]
    [key: string]: any
}

export interface DropdownProps {
    trigger?: 'hover'|'click'|'contextMenu'|'custom',
    align?: 'bottom' | 'bottomLeft' | 'bottomRight' | 'right' | 'rightBottom' | 'left' | 'leftBottom' | 'top' |'topLeft'|'topRight'| 'rightTop' | 'leftTop',
    menu?: JSX.Element,
    modelValue?: boolean,
    theme?: string|'dark'|'light'|'primary'|'success'|'warning'|'error'|'info'|'blue'|'green'|'red'|'yellow'|'pink'|'magenta'|'volcano'|'orange'|'gold'|'lime'|'cyan'|'geekblue'|'purple',
    data?: DropdownNode[]
    disabled?: boolean,
    revers?: boolean,
    handler?: string,
    fixWidth?: boolean,
    gradient?: string[],
    color?: string,
    arrow?: boolean
    offset?: number
    position?: DropdownPosition,
    onBeforeDrop?: (visible: boolean) => boolean,
    transfer?: boolean,
}

export default defineComponent({
    name: 'Dropdown',
    props: {
        trigger: {type: String as PropType<DropdownProps['trigger']>, default: 'hover'},
        align: {type: String as PropType<DropdownProps['align']>, default: 'bottom'},
        theme: {type: String as PropType<DropdownProps['theme']>},
        menu: {type: Object as PropType<DropdownProps['menu']>},
        modelValue: {type: Boolean as PropType<DropdownProps['modelValue']>, default: false},
        disabled: {type: Boolean as PropType<DropdownProps['disabled']>, default: false},
        revers: {type: Boolean as PropType<DropdownProps['revers']>, default: true},
        handler: {type: String as PropType<DropdownProps['handler']>, default: undefined},
        fixWidth: {type: Boolean as PropType<DropdownProps['fixWidth']>, default: false},
        gradient: {type: Array as PropType<DropdownProps['gradient']>, default: () => []},
        color: {type: String as PropType<DropdownProps['color']>, default: undefined},
        arrow: {type: Boolean as PropType<DropdownProps['arrow']>, default: false},
        offset: {type: Number as PropType<DropdownProps['offset']>, default: 0},
        position: {type: Object as PropType<DropdownProps['position']>, default: undefined},
        onBeforeDrop: {type: Function as PropType<DropdownProps['onBeforeDrop']>},
        data: {type: Array as PropType<DropdownProps['data']>, default: undefined},
        transfer: {type: Boolean as PropType<DropdownProps['transfer']>, default: true},
    },
    emits: ['select', 'update:modelValue', 'mouseClick', 'visibleChange'],
    setup (props: DropdownProps, {attrs, emit, slots, expose}) {
        const visible = ref(props.modelValue);
        const opened = ref(visible);
        const placement = ref(props.align);
        const _ = ref("_");
        let nextElementSibling: any;
        let targetEle: any;
        const target = ref<HTMLElement>();
        let timer: any;
        const wrap = ref<HTMLElement>();
        const zindex = usezIndex();
        const revers = props.revers ?? true;
        const classList = computed(() => ({
            'cm-dropdown': true,
            [attrs.class as string]: true,
            [`cm-dropdown-${props.theme}`]: props.theme,
            'cm-dropdown-with-arrow': props.arrow,
        }));
        let lastEventTriggerTarget: any;

        let cleanup: any = null;
        const transition = useTransition({
            el: ()=> wrap.value,
            startClass: 'cm-dropdown-visible',
            activeClass: 'cm-dropdown-open',
            onLeave: () => {
                opened.value = false;
                if (cleanup) {
                    cleanup();
                }
                emit('visibleChange', false);
            },
            onEnter: () => {
                opened.value = true;
                // 监控元素移动
                cleanup = useMoveObserver(wrap.value, () => {
                    _.value = uuidv4();
                });
                emit('visibleChange', true);
            }
        });

        watch(() => props.modelValue, (val) => {
            visible.value = val;
        });

        watch(visible, (v) => {
            if (v) {
                transition.enter();
            } else {
                transition.leave();
            }
        });

        // 防抖
        const clearDelayTimer = () => {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
        };

        // 点击显示
        const onMouseClick = (e: any) => {
            if (!nextElementSibling.contains(e.target)) {
                return false;
            }
            if (props.handler) {
                const te = document.querySelector(props.handler);
                if (!te) {
                    return;
                }
                if (!e.target.closest(props.handler) && !te.contains(e.target)) {
                    return;
                }
            }
            if (props.disabled) {
                return;
            }

            e.preventDefault && e.preventDefault();
            e.stopPropagation && e.stopPropagation();
            targetEle = e.target;
            emit('mouseClick', e);

            const ret = props.onBeforeDrop && props.onBeforeDrop(visible.value);
            if (ret === undefined || ret) {
                // 触发的对象不一致，则重新定位，可能是使用handle进行触发,有多个触发元素的情况
                if (props.handler && lastEventTriggerTarget !== e.target.closest(props.handler)) {
                    _.value = uuidv4();
                }
                visible.value = true;
                emit('update:modelValue', visible.value);
                if (props.handler) {
                    lastEventTriggerTarget = e.target.closest(props.handler);
                }
            }
        };

        const onMouseEnter = () => {
            if (props.disabled) {
                return;
            }
            if (props.trigger === 'hover') {
                clearDelayTimer();
                visible.value = true;
                emit('update:modelValue', true);
            }
        };
        const onMouseLeave = () => {
            if (props.disabled) {
                return;
            }
            if (props.trigger === 'hover') {
                timer = setTimeout( () => {
                    visible.value = false;
                    emit('update:modelValue', false);
                }, 200);
            }
        };

        const getOffsetWidth = (align: string, rect: any) => {
            if (align === 'bottomRight' || align === 'topRight') {
                return 0;
            }
            if (align === 'top' || align === 'bottom') {
                return rect.width / 2;
            }
            if (align === 'topLeft' || align === 'bottomLeft') {
                return rect.width;
            }
            if (align === 'left' || align === 'leftTop' || align === 'leftBottom') {
                return 0;
            }
            if (align === 'right' || align === 'rightTop' || align === 'rightBottom') {
                return rect.width;
            }
        };

        const getOffsetHeight = (align: string, rect: any) => {
            if (align === 'leftBottom' || align === 'rightBottom') {
                return 0;
            }
            if (align === 'top' || align === 'topLeft' || align === 'topRight') {
                return 0;
            }
            if (align === 'leftTop' || align === 'rightTop') {
                return rect.height;
            }
            if (align === 'left' || align === 'right') {
                return rect.height / 2;
            }
            if (align === 'bottom' || align === 'bottomLeft' || align === 'bottomRight') {
                return rect.height;
            }
        };

        const getPositionByPlacement = (align: DropdownProps['align'], te: any) => {
            const parent = te.offsetParent;
            if (!parent) {
                return;
            }
            const parentPos = parent.getBoundingClientRect();
            const pos: any = useAlignPostion(align as string, te);
            if (props.transfer) {
                const targetReact = te.getBoundingClientRect();
                pos.top = pos.top + document.documentElement.scrollTop;
                pos.left = pos.left + document.documentElement.scrollLeft;
                props.fixWidth ? pos['min-width'] = targetReact.width + 'px' : false;
            } else {
                pos.top = pos.top + parent.scrollTop - parentPos.top;
                pos.left = pos.left + parent.scrollLeft - parentPos.left;
            }
            return pos;
        };

        const posStyle = ref({});
        watchEffect(() => {
            _.value;
            if (opened.value && nextElementSibling){
                let te = nextElementSibling;
                if (props.handler) {
                    te = targetEle?.closest(props.handler);
                }
                if (!te) {
                    return;
                }
                const parent = te.offsetParent;
                if (!parent) {
                    return;
                }
                if (props.position) {
                    const pos = {
                        left: props.position.x + 'px',
                        top: props.position.y + 'px'
                    };
                    Object.assign(pos, attrs.style || {}, {
                        '--cui-dropdown-background-color': isColor(props.theme) ? props.theme : '',
                        '--cui-dropdown-text-color': props.color,
                        '--cui-dropdown-offset': `${props.offset}px`,
                    });
                    posStyle.value = pos;
                    return pos;
                }
                const parentPos = parent.getBoundingClientRect();
                let pos: any = useAlignPostion(props.align, te);
                const originTop = pos.top;
                const originLeft = pos.left;
                if (props.transfer) {
                    const targetReact = te.getBoundingClientRect();
                    pos.top = pos.top + document.documentElement.scrollTop;
                    pos.left = pos.left + document.documentElement.scrollLeft;
                    props.fixWidth ? pos['min-width'] = targetReact.width + 'px' : false;
                } else {
                    pos.top = pos.top + parent.scrollTop - parentPos.top;
                    pos.left = pos.left + parent.scrollLeft - parentPos.left;
                }

                const rect = wrap.value.getBoundingClientRect();
                const offsetWidth = getOffsetWidth(props.align, rect);
                const offsetHeight = getOffsetHeight(props.align, rect);
                const h = originTop + offsetHeight;
                const w = originLeft + offsetWidth;

                const containerHeight = window.innerHeight || document.documentElement.clientHeight;
                const containerWidth = window.innerWidth || document.documentElement.clientWidth;

                if (revers) {
                    let newAlign: DropdownProps['align'] = props.align;
                    if (h > containerHeight) {
                        if (props.align === 'bottom' || props.align === 'bottomLeft' || props.align === 'bottomRight') {
                            newAlign = props.align.replace('bottom', 'top') as DropdownProps['align'];
                        } else if (props.align === 'leftTop' || props.align === 'left') {
                            newAlign = 'leftBottom' as DropdownProps['align'];
                        } else if (props.align === 'right' || props.align === 'rightTop') {
                            newAlign = 'rightBottom' as DropdownProps['align'];
                        }
                    }
                    // align 为 top bottom topLeft bottomLeft right rightTop rightBottom 存在该情况
                    if (w > containerWidth - 5) {
                        if (props.align === 'bottom' || props.align === 'bottomLeft') {
                            // pos.left = pos.left - (rect.width - targetRect.width) / 2;
                            newAlign = 'bottomRight' as DropdownProps['align'];
                        } else if (props.align === 'top' || props.align === 'topLeft') {
                            // pos.left = pos.left - rect.width + targetRect.width;
                            newAlign = 'topRight' as DropdownProps['align'];
                        } else if (props.align === 'right') {
                            // pos.left = pos.left - rect.width - targetRect.width;
                            newAlign = 'left' as DropdownProps['align'];
                        } else if (props.align === 'rightTop') {
                            // pos.left = pos.left - rect.width - targetRect.width;
                            newAlign = 'leftTop' as DropdownProps['align'];
                        }
                    }
                    if (newAlign !== props.align) {
                        pos = getPositionByPlacement(newAlign, te);
                        placement.value = newAlign;
                    } else {
                        pos = getPositionByPlacement(props.align, te);
                        placement.value = props.align;
                    }
                }
                pos.top = pos.top + 'px';
                pos.left = pos.left + 'px';

                pos['z-index'] = zindex;

                Object.assign(pos, attrs.style || {}, {
                    '--cui-dropdown-background-color': isColor(props.theme) ? props.theme : '',
                    '--cui-dropdown-text-color': props.color,
                    '--cui-dropdown-offset': `${props.offset}px`,
                });

                posStyle.value = pos;
                // return pos;
            }
        });

        expose({
            update: () => {
                _.value = uuidv4();
            }
        });

        // 响应尺寸变化更新位置
        const onWrapEntry = useDebounce((entry: ResizeObserverEntry) => {
            _.value = uuidv4();
        }, 100);

        let removeClickOutside: any;
        onMounted(() => {
            nextElementSibling = target.value.nextElementSibling;
            target.value.parentNode.removeChild(target.value);
            if (nextElementSibling) {
                if (props.trigger === 'hover') {
                    nextElementSibling.addEventListener('mouseenter', onMouseEnter, false);
                    nextElementSibling.addEventListener('mouseleave', onMouseLeave, false);
                }
                if (props.trigger === 'click' || props.trigger === 'custom') {
                    document.addEventListener('click', onMouseClick);
                    if (props.trigger === 'click') {
                        const other = props.handler ? nextElementSibling.querySelectorAll(props.handler) : nextElementSibling;
                        removeClickOutside = useClickOutside([wrap.value, other], () => {
                            visible.value = false;
                            emit('update:modelValue', false);
                        });
                    }
                }
                if (props.trigger === 'contextMenu') {
                    document.addEventListener('contextmenu', onMouseClick);
                    const other = props.handler ? nextElementSibling.querySelectorAll(props.handler) : nextElementSibling;
                    removeClickOutside = useClickOutside([wrap.value, other], () => {
                        visible.value = false;
                        emit('update:modelValue', false);
                    });
                }
            }

            const ro = new ResizeObserver((entries) => {
                entries.forEach((entry) => onWrapEntry(entry));
            });
            // 目标元素尺寸变化需要更新位置
            ro.observe(nextElementSibling);

            onBeforeUnmount(() => {
                ro.disconnect();
            });
        });

        onBeforeUnmount(() => {
            if (nextElementSibling) {
                if (props.trigger === 'hover') {
                    nextElementSibling.removeEventListener('mouseenter', onMouseEnter);
                    nextElementSibling.removeEventListener('mouseleave', onMouseLeave);
                }
                if (props.trigger === 'click' || props.trigger === 'custom') {
                    document.removeEventListener('click', onMouseClick);
                }
                if (props.trigger === 'contextMenu') {
                    document.removeEventListener('contextmenu', onMouseClick);
                }
            }
            removeClickOutside && removeClickOutside();
            if (cleanup) {
                cleanup();
            }
        });

        const onSelect = (name: string) => {
            emit('select', name);
            visible.value = false;
            emit('update:modelValue', false);
        };

        const renderMenu = (arr?: DropdownNode[]) => {
            if (arr) {
                return <DropdownMenu>
                    {
                        arr.map(item => {
                            const {title, children, ...rest} = item;
                            return <DropdownItem {...rest} data={item} arrow={!!children?.length}>
                                {title}
                                {
                                    children?.length ? renderMenu(children!) : null
                                }
                            </DropdownItem>;
                        })
                    }
                </DropdownMenu>;
            }
            return null;
        };

        provide('dropdownConext', {
            onSelect,
            gradient: props.gradient,
            color: props.color
        });

        const id = 'cm-dropdown-portal';
        return () => <>
            <span ref={target} style={{ display: 'none' }} />
            {slots.default?.()}
            {
                props.transfer
                    ? <Teleport to={usePortal(id, id)}>
                        <div style={posStyle.value} class={classList.value} x-placement={placement.value}
                            onMouseenter={onMouseEnter} onMouseleave={onMouseLeave} ref={wrap}>
                            {props.menu || (slots.menu ? slots.menu() : null)}
                            {renderMenu(props.data)}
                            {
                                props.arrow ? <svg width="24" height="8" xmlns="http://www.w3.org/2000/svg" class="cm-dropdown-arrow">
                                    <path d="M0.5 0L1.5 0C1.5 4, 3 5.5, 5 7.5S8,10 8,12S7 14.5, 5 16.5S1.5,20 1.5,24L0.5 24L0.5 0z" opacity="1" />
                                    <path d="M0 0L1 0C1 4, 2 5.5, 4 7.5S7,10 7,12S6 14.5, 4 16.5S1,20 1,24L0 24L0 0z" />
                                </svg>
                                    : null
                            }
                        </div>
                    </Teleport>
                    : <div style={posStyle.value} class={classList.value} x-placement={placement.value}
                        onMouseenter={onMouseEnter} onMouseleave={onMouseLeave} ref={wrap}>
                        {props.menu || (slots.menu ? slots.menu() : null)}
                        {renderMenu(props.data)}
                        {
                            props.arrow ? <svg width="24" height="8" xmlns="http://www.w3.org/2000/svg" class="cm-dropdown-arrow">
                                <path d="M0.5 0L1.5 0C1.5 4, 3 5.5, 5 7.5S8,10 8,12S7 14.5, 5 16.5S1.5,20 1.5,24L0.5 24L0.5 0z" opacity="1" />
                                <path d="M0 0L1 0C1 4, 2 5.5, 4 7.5S7,10 7,12S6 14.5, 4 16.5S1,20 1,24L0 24L0 0z" />
                            </svg>
                                : null
                        }
                    </div>
            }
        </>;
    }
});
